from flask import Blueprint,render_template,redirect,url_for,flash,session,current_app,request,jsonify,g,make_response
from app.forms import RegisterForm,LoginForm1,LoginForm2,UploadPortraitForm,ModifyPasswordForm
from app.models import User # 从app文件夹里models.py中导入User模型(数据表)。刚创建的模型第一次数据库迁移前，需在代码里加载一次(如这句导入)，否则不知道模型和谁关联
from app.extensions import db,pictures,cache # 从app文件夹里extensions.py中导入db对象和pictures对象和cache对象
from app.mail import send_mail2 # 从app文件夹里mail.py中导入send_mail2()函数
from datetime import timedelta
from flask_login import login_user,logout_user,current_user,login_required # current_user表示当前登录的用户，这里导入不导入都行，在myself_base.html和profile.html中使用(其实任意地方都可以直接用)
from app.util import random_string
import os
from PIL import Image # 只支持python2，python3需要安装pillow库
import re
from app.util import generate_verification_code_image
from io import BytesIO

user=Blueprint('user',__name__) # 创建蓝本对象，第一个参数是蓝本名，不能与其他蓝本名重复；第二个参数是要导入蓝本的模块的名；第三个参数是统一给蓝本里路由添加的前缀，可不写
'''
钩子函数
直接应用在app上:
before_first_request
before_request
after_request
teardown_request
应用在视图函数:
客户端向服务器发送请求时，若是第一次发送，服务器先执行被@before_app_first_request装饰的视图函数；无论是不是第一次发送，接着服务器执行@before_app_request；然后服务器执行@after_app_request；最后执行@teardown_app_request
before_app_first_request，仅第一次访问时才执行
before_app_request，每次请求时都执行，且先执行这个，再执行请求对应的内容，可在这里进行用户权限验证，如验证是否已登录，未登录时可以返回一个页面。这里请求已到达服务器，所以被@before_app_request装饰的视图函数里可直接使用request对象
after_app_request，被@before_app_request装饰的函数执行完后，向客户端返回时执行，视图函数里需传入response，因被@before_app_request装饰的视图函数会返回response，即服务器对客户端的响应，如果不传入，说明丢弃了response。需对response处理时才使用
teardown_app_request
'''


@user.route('/login/',methods=['GET','POST'])
def login():
    if session.get('_user_id'): # 已登录时可在地址栏手动访问登录页，所以这里加判断。'_user_id'的意思在后面搜。
        flash('您已登录，无需再登录')
        return redirect(url_for('main.index'))
    way=request.args.get('way') # 注意返回的是字符串类型，后面判断way的值时要给数字加引号
    if way=='2': # login.html中定义了way为1时是密码登录，为2时是短信登录。只要way不为2就显示密码登录表单，第一次请求登录页时(request中没有way)默认显示密码登录表单
        form=LoginForm2()
    else:
        form=LoginForm1()
    if form.validate_on_submit():
        if isinstance(form,LoginForm1): # 密码登录
            u=User.query.filter(User.username==form.username.data).first()
            if not u or u.is_deleted==True:
                flash('用户名不存在') # 最好提示'用户名或密码错误'，防止确认用户名正确后暴破密码。现在这样写是方便测试那些登录失败的判断是否生效
            elif not u.is_activated:
                flash('帐号未激活，请点击邮箱中的激活链接激活后再登录')
            elif u.verify_password(form.password.data):
                '''
                session['username']=form.username.data # 根据session是否存在判断是否已登录，以判断是否显示退出按钮和个人信息按钮
                session.permanent=True # 设为永久生效，先设置这个，自定义的过期时间才有效。写app.config['SESSION_PERMANENT']=True在Flask1.1.2无效
                current_app.permanent_session_lifetime=timedelta(days=1) # session有效期，默认31天(点击config查看或直接打印等号左面)，等号右面只写整数时单位秒，也可以写timedelta类型(单位需指定，可点timedelta查看)。不设置过期时间时浏览器关闭后失效；设置时对普通模式有效，无痕模式无论是否到时间，只要关闭浏览器就失效。from https://flask.palletsprojects.com/en/1.1.x/api/#flask.session.permanent
                # current_app.config['PERMANENT_SESSION_LIFETIME']=timedelta(days=1) # 过期时间也可以这样设置
                '''
                login_user(u,remember=form.remember.data,duration=timedelta(days=7)) # 用户登录，第二个参数是是否自动登录，第三个参数是有效期。使用Flask_Login管理，上面的注释是不用Flask_Login的写法
                del session['verification_code']
                flash('登录成功')
                # 未对next参数值作验证，避免被URL注入攻击 from http://www.bjhee.com/flask-ext8.html
                return redirect(request.args.get('next') or url_for('main.index')) # args里存放所有GET参数(form里存放POST参数)，里面如果有next参数(介绍在extensions.py)(未登录时访问被@login_required装饰的路由才有next参数，后面列表required_login_list配合钩子函数的写法，登录后不会跳转到登录前的页面)，说明是在未登录状态访问了"需要登录才能访问的页面"，登录后需跳回刚才的页面，即需要登录才能访问的页面。没有next参数登录后就跳转到首页
            else:
                flash('密码错误') # 最好提示'用户名或密码错误'，防止确认用户名正确后暴破密码。现在这样写是方便测试那些登录失败的判断是否生效
        elif isinstance(form,LoginForm2): # 短信登录
            '''用session保存手机号与验证码的键值对
            if form.verification_code.data==session.get(form.phone.data): # 发送验证码时已将手机号和验证码作为键值对存入session。先判断验证码是否正确，正确的话再判断输入的手机号是否是接收验证码的手机号，都正确再查数据库，减轻数据库压力
                if form.phone.data in session:
                    u=User.query.filter(User.phone==form.phone.data).first() # 虽然发验证码时已判断手机号是否存在，但登录时还要判断，保险
                    if not u or u.is_deleted==True:
                        session.clear() # 登录失败，若不再登录，需删除所有session，同时删除服务器里用于存放该session的内存空间和浏览器里的cookie
                        # clear()写在flash()后时，会删除前面设置的flash消息，导致不显示flash消息，所以若和flash()一起使用，需写在flash()前面
                        flash('手机号不存在')
                    elif not u.is_activated:
                        session.clear()  # 同上
                        flash('帐号未激活，请点击邮箱中的激活链接激活后再登录')
                    else:
                        login_user(u,remember=form.remember.data,duration=timedelta(days=7))
                        del session[form.phone.data] # 登录成功后删除session里的手机号和验证码键值对，防止下次登录时不点发送验证码，使用原来的验证码就能登录。加这句的原因在视图函数logout()里的注释
                        flash('登录成功')
                        return redirect(request.args.get('next') or url_for('main.index'))
                # else: # 输入的验证码是刚发送的验证码，但手机号又改成另一个手机号了，再点登录。其实不用加这个判断，因上一层判断验证码时，是根据输入的手机号去session里取验证码，session里没有被修改后的手机号
                #     flash('手机号错误')
            else:
                # session.clear() 可能是验证码打错了，所以这里登录失败时不删除session。写在flash()后时，会删除前面设置的flash消息，导致不显示flash消息，所以若和flash()一起使用，需写在flash()前面
                flash('验证码或手机号错误') # 注释同上面注释掉的else
            '''
            if form.verification_code.data==cache.get(form.phone.data): # 从redis中取验证码。若输入正确的手机号与验证码，点登录后抓包改手机号，不知道能否成功登录别人的帐号
                u=User.query.filter(User.phone==form.phone.data).first()
                if not u or u.is_deleted==True:
                    flash('手机号不存在')
                elif not u.is_activated:
                    flash('帐号未激活，请点击邮箱中的激活链接激活后再登录')
                else:
                    login_user(u,remember=form.remember.data,duration=timedelta(days=7))
                    cache.delete(form.phone.data)
                    flash('登录成功')
                    return redirect(request.args.get('next') or url_for('main.index'))
            else:
                flash('验证码或手机号错误')
    return render_template('user/login.html',form=form)
'''
点击创建cache处的Cache查看支持的方法，在源码的@property处。也可以看https://flask-caching.readthedocs.io/en/latest/api.html#cache-api
缓存键值对
设置
cache.set(key,value,timeout=) 第三个参数是过期时间，单位秒
cache.set_many([(key,value),(key,value)...])
获取
cache.get(key)
cache.get_many(key1,key2...)
删除
cache.delete(key)
cache.delete_many(key1,key2...)
cache.clear()
缓存视图函数
在要缓存的视图函数前加@cache.cached(timeout=120)。若某个页面加载的资源多，加载速度慢，加上缓存后，第一次访问这个页面时，加载完成后会将内容缓存到redis中，缓存过期之前再访问这个页面时无需再等待加载，直接使用缓存中的数据。测试时可以用sleep模拟等待加载，这样下次访问时不触发sleep
'''


@user.route('/send_verification_code/') # 接收前端Ajax发来的手机号，返回是否发送成功。发送时(login.html里)用的GET，所以这里接收也用GET
def send_verification_code():
    if request.headers.get("X-Requested-With") == "XMLHttpRequest": # 说明是Ajax请求，详细看verify_phone路由的注释
        # 获取发送的Ajax请求中的字典的值，字典中键是phone
        phone=request.args.get('phone') # 使用GET发送Ajax请求时这样获取字典的值。这里是获取name为phone的表单的输入框的值，笔记在01-Hello World搜request.args
        # phone=request.form['phone'] # 使用POST发送Ajax请求时这样获取字典的值
        # 无论get还是post参数，都能通过request.values.get('')获取
        u=User.query.filter(User.phone==phone,User.is_deleted==False).first()
        if u:
            # session[phone]='123456' # 将验证码与手机号绑定，防止登录时用当前手机号收到的验证码，和另外一个手机号进行登录。登录成功后要立即删除这个session，原因在删除处。session在一个文件内是全局的，其他地方可以直接访问。相关链接(不是来自这里)https://www.v2ex.com/t/349677
            cache.set(phone,'123456',timeout=300) # 第三个参数是过期时间，单位秒。原来的session保存手机号与验证码的键值对，现在将它放在redis缓存中。点击创建cache处的Cache查看支持的方法，在源码的@property处。也可以看https://flask-caching.readthedocs.io/en/latest/api.html#cache-api
            # 定义flag为200表示验证码发送成功；为400表示该号码未注册，验证码未发送
            # return jsonify(flag=200,message='验证码发送成功') # 需向"前端里向这里发来Ajax请求的地方"返回json格式，可以直接返回{key:value}，也可以用jsonify()，它会将传入的参数自动转为json格式
            return jsonify(flag=200, message='验证码发送成功，5分钟内有效') # 上一行是用session时用
        else:
            return jsonify(flag=400,message='该号码未注册，验证码未发送')
    else:
        return render_template('main/index.html')


@user.route('/send_verification_code_to_email/',methods=['GET','POST']) # 接收前端Ajax发来的邮箱，返回是否发送成功。前端可设置失去焦点时验证，比在forms.py中写自定义验证(验证用户名和邮箱处使用)有更好的用户体验。发送时(modify_email.html里)用的POST，所以这里接收也用POST
def send_verification_code_to_email():
    if request.headers.get("X-Requested-With") == "XMLHttpRequest": # 说明是Ajax请求，详细看verify_phone路由的注释
        # 获取发送的Ajax请求中的字典的值，字典中键是email
        email=request.form['email']
        # email=request.args.get('email') # 使用GET发送Ajax请求时这样获取字典的值
        # 无论get还是post参数，都能通过request.values.get('')获取
        result=re.match(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$',email)
        if result:
            u=User.query.filter(User.email==result.group()).first() # 使用正则过滤后的邮箱字符串去查询数据库，更安全。group()返回匹配的字符串部分，即合法的邮箱字符串。
            if not u:
                # 定义flag为200表示验证码发送成功；为300表示该邮箱已被使用；为400表示新邮箱格式不正确
                verification_code=random_string(length=6)
                # session[result.group()]=verification_code # 将验证码与邮箱绑定，防止提交时用刚才的验证码，和另一个非刚才接收验证码的邮箱，进行提交。改完邮箱(写入数据库)后要立即删除这个session，原因在删除处。session在一个文件内是全局的，其他地方可以直接访问。相关链接(不是来自这里)https://www.v2ex.com/t/349677
                cache.set(result.group(),verification_code,timeout=3600)
                send_mail2('修改邮箱',result.group(),'modify_email',username=current_user.username,verification_code=verification_code)
                # return jsonify(flag=200,message='验证码发送成功') # 需向"前端里向这里发来Ajax请求的地方"返回json格式，可以直接返回{key:value}，也可以用jsonify()，它会将传入的参数自动转为json格式
                return jsonify(flag=200, message='验证码发送成功，1小时内有效') # 上一行是用session时用
            else:
                return jsonify(flag=300,message='该邮箱已被使用，请更换邮箱')
        else:
            return jsonify(flag=400, message='新邮箱格式不正确，验证码未发送')
    else:
        return render_template('main/index.html')


@user.route('/logout/')
def logout():
    phone=current_user.phone
    if cache.get(phone): # 登录成功时已立刻删除缓存中的手机号与验证码键值对，退出登录时再验证一下，防止验证码被重复使用
        cache.delete(phone)
    session.clear() # 退出时清除所有session。用这句时不用写下一句(前提是用Flask_Login的login_user()登录，且未勾选自动登录)。写在flash()后时，会删除前面设置的flash消息，导致不显示flash消息，所以若和flash()一起使用，需写在flash()前面
    logout_user() # 退出登录。使用Flask_Login管理，不如上面不用Flask_Login的写法，因如果session里有手动添加的内容，如短信登录时session里的手机号和验证码键值对等，退出时logout_user()不会清除，点击查看它能清除的。但若用Flask_Login的login_user()登录，且勾选了自动登录，则1.只有logout_user()才能退出登录(可点击查看源码里的注释有说)，session.clear()无法退出登录；2.若又需退出登录时删除所有session，即logout_user()和session.clear()都写时，session.clear()只能写在logout_user()前面，写在后面时无法退出登录(注意有两个前提)。
    # 一开始只用了logout_user()，没用session.clear()，短信登录成功后也没手动删除session里的手机号和验证码键值对，导致退出登录后，再登录时不点发送验证码，使用原来的验证码就能登录。所以后来这里加了删除所有session，短信登录成功后也删了session里的手机号和验证码键值对
    flash('已退出帐号')
    return redirect(url_for('main.index'))


@user.route('/register/',methods=['GET','POST'])
def register(): # 注册时如果填写的用户信息已在数据库，且数据库中的is_deleted字段为True，未适配这种场景，即未判断用户是否已注销
    form=RegisterForm()
    if form.validate_on_submit(): # 如果是POST请求且校验通过
        u=User(username=form.username.data,password=form.password.data,email=form.email.data,phone=form.phone.data) # 根据表单数据创建User对象，即新注册的用户。由于在models.py中设置了加密密码，这里创建对象时密码也是加密后的。注意用password而不是password_hash，详情在models.py中装饰器上一行的注释
        db.session.add(u) # 将新注册的用户的信息存入数据库。本质是保存到session缓存，commit()后才存入数据库
        db.session.commit() # 发送激活邮件时，构造激活地址时需要传入用户id(id告诉服务器是谁激活)，不手动提交的话请求结束才自动提交，那样构造激活地址时数据库里还没新用户数据，所以无法获取id，所以这里手动提交
        del session['verification_code']
        # activate_url=url_for('user.activate',uid=u.id,_external=True) # 构造完整激活地址(含主机和端口)。未设置token时使用
        token=u.generate_activate_token() # 生成用于激活帐号的token，防止激活链接中明文存放新用户的id，可传入过期时间，定义时已设默认值
        activate_url=url_for('user.activate',token=token,_external=True)
        send_mail2('帐号激活',form.email.data,'activate',username=form.username.data,activate_url=activate_url) # 发送激活邮件
        flash('注册成功，激活邮件已发送到你的邮箱，需点击邮件中的激活链接激活后才能使用该帐号！')
        return redirect(url_for('main.index')) # url_for()里写蓝本名.视图函数名。只写.视图函数名表示加载当前文件的蓝本
    return render_template('user/register.html',form=form)


'''未设置token时使用
@user.route('/activate/<uid>')
def activate(uid):
    u=User.query.get(uid)
    if not u: # 数据库中无此用户信息
        flash('用户不存在，激活失败！')
        return redirect(url_for('main.index'))
    if not u.is_activated: # 此用户未激活
        u.is_activated=True
        db.session.add(u)
        flash('激活成功！')
    return redirect(url_for('user.login'))
'''
@user.route('/activate/<token>') # 已设置token时使用
def activate(token):
    if User.check_activate_token(token):
        flash('激活成功！')
        return redirect(url_for('user.login'))
    else: # token错误，即数据库中无此用户信息
        flash('激活失败！')
        return redirect(url_for('main.index'))


@user.route('/required_login/')
@login_required # 保护当前路由，需登录才能访问
def required_login():
    return '这个页面需要登录才能访问，你看到了说明已登录'


@user.route('/profile/',methods=['GET','POST']) # 个人信息页。文件上传是POST请求
@login_required
def profile():
    form=UploadPortraitForm()
    if form.validate_on_submit():
        file_extension=os.path.splitext(form.portrait.data.filename)[1] # 表单中可以通过form.portrait.data获取提交的数据
        filename=random_string()+file_extension
        pictures.save(form.portrait.data,name=filename) # 保存图片。图片是form表单中的portrait字段的数据，保存位置已在config.py中UPLOADED_PICTURES_DEST设置，两个下划线之间需是创建文件上传对象时(extensions.py中)设置的对象名，转大写。
        file_path=os.path.join(current_app.config['UPLOADED_PICTURES_DEST'],filename)
        img=Image.open(file_path) # 打开上传的图片
        img.thumbnail((120,120)) # 设置图片尺寸，可用于生成缩略图或缩放图片
        img.save(file_path) # 覆盖上面保存的图片原文件
        if current_user.portrait_file_name!='noavatar_middle.gif': # 不是默认头像时，上传新头像后删除旧头像
            os.remove(os.path.join(current_app.config['UPLOADED_PICTURES_DEST'],current_user.portrait_file_name))
        current_user.portrait_file_name=filename # 保存用户的头像文件名到数据库
        db.session.add(current_user)
        flash('头像上传成功')
    portrait_url=pictures.url(current_user.portrait_file_name) # 从数据库获取头像文件名，再根据文件名构造URL。若用户未上传头像，则没有if里的filename变量，所以括号里不能写current_user.filename，此时应显示默认头像，所以从数据库获取
    t=current_user.register_time+timedelta(hours=8) # 数据库里是utc时间(User模型里设置的)，比东八区晚8小时，前端显示时要显示8小时后的时间。若前端用moment().format()，则传值时不用加8
    return render_template('user/profile.html',form=form,portrait_url=portrait_url,t=t)


@user.route('/modify_password/',methods=['GET','POST'])
@login_required
def modify_password():
    form=ModifyPasswordForm()
    if form.validate_on_submit():
        '''个人觉得是验证前端数据，所以不安全。这里不验证的话，也可以在forms.py中修改密码表单中自定义验证
        if form.new_password.data==form.old_password.data:
            flash('新密码与旧密码不能相同，请重新修改')
            return redirect(url_for('user.modify_password'))
        '''
        current_user.password=form.new_password.data # 注意修改密码时用password而不是password_hash，详情在models.py中装饰器上一行的注释
        db.session.add(current_user)
        logout_user() # 退出登录
        flash('修改密码成功，请重新登录')
        return redirect(url_for('user.login'))
    return render_template('user/modify_password.html',form=form)


@user.route('/verify_phone/',methods=['GET','POST']) # Ajax前端验证手机号是否已使用。前端可设置失去焦点时验证，比在forms.py中写自定义验证(验证用户名和邮箱处使用)有更好的用户体验。发送时(register.html里)用的POST，所以这里接收也用POST
def verify_phone():
    '''
    if request.is_xhr: 类似Django里if request.is_ajax()，判断是否是Ajax请求，但在Werkzeug大于0.16.1的版本里弃用了，可以退回0.16.1，或用注释下一行的写法。
    上一行from https://www.cnblogs.com/hszstudypy/p/13284982.html和https://stackoverflow.com/questions/60992849/attributeerror-request-object-has-no-attribute-is-xhr
    和https://stackoverflow.com/questions/60131900/weird-is-xhr-error-when-deploying-flask-app-to-heroku，这个链接是前一个链接里回答里的第二个here(超链接)，注释下一行的写法来自这里的回答
    补充XHR和XMLHttpRequest的介绍
    https://www.w3cschool.cn/ajax/ajax-xmlhttprequest-send.html和https://zhuanlan.zhihu.com/p/163718536
    也可以判断request里有没有Ajax请求传的参数(不推荐，原因在下一行)，有就说明是Ajax请求访问的当前路由，没有说明可能是直接在浏览器输入Ajax接收处的路由，可以返回指定页来防止这种情况(无论哪种判断方式，不写而直接在浏览器访问路由会报错)。也可以根据页面里的模板报错状态码(不是浏览器网络里的状态码)写对应的自定义错误页。
    Ajax传的参数可以伪造(不知道下面代码里的写法是否可以伪造)，如，若Ajax请求是以GET发送的，则在浏览器输入Ajax接收处的路由时跟上参数和值就行，就能"正常"访问这个路由，POST同理
    '''
    if request.headers.get("X-Requested-With") == "XMLHttpRequest": # 说明是Ajax请求
        # 获取发送的Ajax请求中的字典的值，字典中键是phone
        phone=request.form['phone'] # 使用POST发送Ajax请求时这样获取字典的值。这里是获取name为phone的表单的输入框的值，笔记在04-form搜request.form
        # phone=request.args.get('phone') # 使用GET发送Ajax请求时这样获取字典的值
        # 无论get还是post参数，都能通过request.values.get('')获取
        u=User.query.filter(User.phone==phone).first()
        if u:
            # 定义flag为0表示该手机号已注册，需更换手机号；为1表示可以注册
            return jsonify(flag=0,message='该手机号已被使用，请更换手机号') # 需向"前端里向这里发来Ajax请求的地方"返回json格式，可以直接返回{key:value}，也可以用jsonify()，它会将传入的参数自动转为json格式
        else:
            return jsonify(flag=1,message='该手机号可用')
    else: # 说明不是Ajax请求，可能是在浏览器输入Ajax接收处的路由，不写if-else时直接在浏览器访问路由会报错，可以返回指定页防止报错
        return render_template('main/index.html')


required_login_list=['/user/modify_email/','/user/logout/','/article/publish_article2/'] # 需要登录才能访问的路由(不仅限于当前文件里的路由，其他文件里的也可以，如发布文章)，写在这里。和写在路由里一样，末尾要加/，这样访问时末尾加不加/都行。这是不用@login_required的写法，登录后不会像@login_required那样跳回登录前的页面，因这种方式没有next参数


@user.before_app_first_request
def first_request():
    print('这是仅第一次访问时才执行的')


@user.before_app_request
def before_app_request():
    print('每次请求时都执行，且先执行这个，再执行请求对应的内容')
    print('请求的路径是'+request.path) # 请求已到达服务器，所以有request对象。详细介绍在前面搜这个装饰器
    if request.path in required_login_list: # 限制某些需登录才能访问的路由
        id=session.get('_user_id') # 使用login_user()登录后，session里有'_id'和'_user_id'，登录不同的用户时'_id'的值一样，'_user_id'的值是当前用户在数据库中的id(models.py中用Flask-Login管理用户登录与退出时的回调函数就是用的这个)，即创建模型时里面的id，后面要从数据库中根据id查用户，所以这里从session里取'_user_id'
        if not id:
            flash('需登录才能访问此页面')
            return redirect(url_for('user.login'))
        else:
            u=User.query.get(id)
            g.user=u # 系统级全局变量g，可以直接在模板中使用(需先导入)。写在这里时，是给g对象动态添加一个user属性，若已登录，访问需登录才能访问的路由时，那些路由里可直接使用g.user，如modify_email路由中，但仅限本次请求中可以用，请求响应后(服务器返回response后)失效。被渲染的html里也可以用，因渲染时是在视图函数内，还未返回给浏览器


@user.route('/modify_email/',methods=['GET','POST'])
def modify_email():
    if request.method=='GET':
        return render_template('user/modify_email.html')
    else: # 参考短信登录代码
        email=request.form['new_email']
        verification_code=request.form.get('verification_code')
        result=re.match(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$',email)
        '''
        if verification_code==session.get(result.group()): # 发送验证码时已将邮箱和验证码作为键值对存入session。先判断验证码是否正确，正确的话再判断输入的邮箱是否是接收验证码的邮箱，
            if result.group() in session:
                current_user.email=result.group()
                db.session.add(current_user)
                del session[result.group()] # 改完邮箱后删除session里的邮箱和验证码键值对，防止下次修改时(且未退出登录过)还是用上次的验证码
                flash('修改邮箱成功')
                return redirect(url_for('user.profile'))
        # session.clear() 可能是验证码打错了，所以这里提交失败时不删除session。写在flash()后时，会删除前面设置的flash消息，导致不显示flash消息，所以若和flash()一起使用，需写在flash()前面
        '''
        if verification_code==cache.get(result.group()):
            current_user.email=result.group()
            db.session.add(current_user)
            cache.delete(result.group())
            flash('修改邮箱成功')
            return redirect(url_for('user.profile'))
        flash('验证码错误')
        return redirect(url_for('user.modify_email'))


@user.after_app_request
def after_app_request(response):
    response.set_cookie('a','value of a')
    print('执行了after_app_request')
    return response


@user.teardown_app_request
def teardown_app_request(response):
    print('执行了teardown_app_request')
    return response


@user.route('/verification_code_image')
def verification_code_image():
    verification_code,image=generate_verification_code_image(4)
    session['verification_code']=verification_code # 多人访问时，验证码会存在各自的session里，不会发生后访问的人的验证码覆盖前面的人的验证码。若放在redis缓存里，写cache.set('verification_code',verification_code,timeout=120)会发生前面说的覆盖的情况，因每次存入redis的键值对的键相同
    buffer=BytesIO()
    image.save(buffer,"PNG") # 将Image对象转为二进制存入buffer。因BytesIO()是在内存中操作，所以实际是存入内存
    buf_bytes=buffer.getvalue() # 从内存中取出bytes类型的图片
    response=make_response(buf_bytes)
    response.headers['Content-Type']='image/png' # 设置请求头，文件格式与前面save时一致
    '''response介绍
    https://www.sohu.com/a/251575938_100120307
    https://blog.csdn.net/fanbaodan/article/details/84846872/
    https://blog.csdn.net/weixin_41665106/article/details/105599235
    https://www.cnblogs.com/zhuchunyu/p/10466509.html
    '''
    # 还可以将buf_bytes转成base64编码的形式并返回，已写博客
    return response